const {PDFDocument} = require("pdf-lib");
const fs = require("fs");

class PdfProcessor {
    constructor() {
    }

    /**
     * 校验字符串中间的短横线：/[0-9]+-[0-9]+/
     * @type {RegExp}
     */
    static patternMiddleHiphen = /[0-9]+-[0-9]+/;

    /**
     * 校验字符串末尾的短横线：/^[0-9]+-$/
     * @type {RegExp}
     */
    static patternEndHiphen = /^[0-9]+-$/;

    /**
     * 校验页码：/^[1-9]{1}[0-9]*$/
     * @type {RegExp}
     */
    static patternInteger = /^[1-9]{1}[0-9]*$/;

    /**
     * 校验页码的范围（该写法不够完善）：/^([1-9]{1}[0-9]*[,-]?)+[0-9]*$/
     * 例：
     如果要删除第2页，则填“2”；
     如果要删除第1-7页，则填“1-7”；
     如果要删除第8页及其后各页，则填“8-”；
     如果要删除第1、3、5页，则填“1,3,5"（注意是英文逗号）；
     如果要删除第1、3、5-10页，则填“1,3,5-10"（注意是英文逗号）；
     * @type {RegExp}
     */
    static patternPages = /^([1-9]{1}[0-9]*[,-]?)+[0-9]*$/;

    /**
     * 将文件与现有pdf文件合并。
     *
     * @param path {string} 目标文件
     * @param doc {PDFDocument} 现有pdf文件
     * @returns {Promise<void>}
     */
    static async #mergeFile(path, doc) {
        const fileSource = await PDFDocument.load(fs.readFileSync(path), {
            ignoreEncryption: true
        });
        const fileSourcePages = await doc.copyPages(fileSource, fileSource.getPageIndices());
        for (let fileSourcePage of fileSourcePages) {
            doc.addPage(fileSourcePage);
        }
    }

    /**
     * 保存PDF文件。
     *
     * @param doc {PDFDocument} 待保存的PDF文件
     * @param path {string} 目标路径
     * @returns {Promise<void>}
     */
    static async #saveFile(doc, path) {
        fs.writeFileSync(path, await doc.save());
    }

    /**
     * 合并PDF文件。
     *
     * @param pathsSource {string[]}
     * @param pathTarget {string}
     * @returns {Promise<void>}
     */
    static async mergeFiles(pathsSource, pathTarget) {
        var DOC = await PDFDocument.create();
        for (let path of pathsSource) {
            await this.#mergeFile(path, DOC);
        }
        await this.#saveFile(DOC, pathTarget);
    }

    /**
     * 将PDF页面插入PDF文件的指定位置。
     *
     * @param pathSrc {string} 待插入文件的路径
     * @param docTgt {PDFDocument} 被插入的PDF文件
     * @param rangeSrc {number[]} 待插入文件的待插入页码范围（0-based），如果要插入全部页码，则输入空数组“[]”，或留空。
     * @param indexTgtStart {number} 被插入文件的起始插入位置。如果要在第3页之后插入，则输入3；如果要在最前面插入，则输入0。
     * @returns {Promise<void>}
     */
    static async #insertPages(pathSrc, docTgt, rangeSrc = [], indexTgtStart = 0) {
        const fileSource = await PDFDocument.load(fs.readFileSync(pathSrc), {
            ignoreEncryption: true
        });
        if (rangeSrc.length === 0) {
            rangeSrc = fileSource.getPageIndices();
        }
        const fileSourcePages = await docTgt.copyPages(fileSource, rangeSrc);
        let ind = indexTgtStart;
        for (let fileSourcePage of fileSourcePages) {
            docTgt.insertPage(ind, fileSourcePage);
            ind++;
        }
    }

    /**
     * 将PDF页面替换PDF文件的指定页面。
     *
     * @param pathSrc {string} 待插入文件的路径
     * @param docTgt {PDFDocument} 被插入的PDF文件
     * @param rangeSrc {number[]} 待插入文件的待插入页码范围（0-based），如果要插入全部页码，则输入空数组“[]”，或留空。
     * @param indexTgtStart {number} 被插入文件的起始插入位置。如果要在第3页之后插入，则输入3；如果要在最前面插入，则输入0。
     * @returns {Promise<void>}
     */
    static async #replacePages(pathSrc, docTgt, rangeSrc = [], indexTgtStart = 0) {
        const fileSource = await PDFDocument.load(fs.readFileSync(pathSrc), {
            ignoreEncryption: true
        });

        if (rangeSrc.length === 0) {
            rangeSrc = fileSource.getPageIndices();
        }

        const fileSourcePages = await docTgt.copyPages(fileSource, rangeSrc);
        let startReplace = rangeSrc.length + indexTgtStart - 1;
        if (startReplace >= docTgt.getPageCount()) {
            startReplace = docTgt.getPageCount() - 1;
        }

        for (let i = startReplace; i >= indexTgtStart; i--) {
            docTgt.removePage(i);
        }

        let ind = indexTgtStart;
        for (let fileSourcePage of fileSourcePages) {
            docTgt.insertPage(ind, fileSourcePage);
            ind++;
        }
    }

    /**
     * 将PDF页面插入PDF文件的指定位置。
     *
     * @param pathSource {string} 待插入文件的路径
     * @param pathTarget {string} 被插入文件的路径
     * @param rangeSource {number[]} 待插入文件的待插入页码范围（0-based），如果要插入全部页码，则输入空数组“[]”，或留空。
     * @param indexTargetStart {number} 被插入文件的起始插入位置。如果要在第3页之后插入，则输入3；如果要在最前面插入，则输入0。
     * @param pathNew {string} 合并后的文件的保存路径
     * @returns {Promise<void>}
     */
    static async insertFile(pathSource, pathTarget, rangeSource = [], indexTargetStart = 0, pathNew = "") {
        const DOC = await PDFDocument.create();
        await this.#insertPages(pathTarget, DOC, [], 0);
        await this.#insertPages(pathSource, DOC, rangeSource, indexTargetStart);
        await this.#saveFile(DOC, pathNew);
    }

    /**
     * 将PDF页面插入PDF文件的指定位置，并替换原有页面。
     *
     * @param pathSource {string} 待插入文件的路径
     * @param pathTarget {string} 被插入文件的路径
     * @param rangeSource {number[]} 待插入文件的待插入页码范围（0-based），如果要插入全部页码，则输入空数组“[]”，或留空。
     * @param indexTargetStart {number} 被插入文件的起始插入位置。如果要在第3页之后插入，则输入3；如果要在最前面插入，则输入0。
     * @param pathNew {string} 合并后的文件的保存路径
     * @returns {Promise<void>}
     */
    static async replaceFile(pathSource, pathTarget, rangeSource = [], indexTargetStart = 0, pathNew = "") {
        const DOC = await PDFDocument.create();
        await this.#insertPages(pathTarget, DOC, [], 0);
        await this.#replacePages(pathSource, DOC, rangeSource, indexTargetStart);
        await this.#saveFile(DOC, pathNew);
    }


    /**
     * 删除PDF文档的指定页面。
     *
     * @param path {string} 待处理PDF文档的路径
     * @param pages {number[]} 待删除的页码范围
     * @param pathNew {string} 新文件的保存路径
     * @returns {Promise<void>}
     */
    static async deletePages(path, pages, pathNew) {
        const file = await PDFDocument.load(fs.readFileSync(path), {
            ignoreEncryption: true
        });
        const DOC = await file.copy();
        for (let i = pages.length - 1; i >= 0; i--) {
            DOC.removePage(pages[i]);
        }
        await this.#saveFile(DOC, pathNew);
    }

    /**
     * 删除PDF文档的指定页面。
     *
     * @param path {string} 待处理PDF文档的路径
     * @param pages {string} 待删除的页码范围
     * @param pathNew {string} 新文件的保存路径
     * @returns {Promise<void>}
     */
    static async deletePages(path, pages, pathNew) {
        const file = await PDFDocument.load(fs.readFileSync(path), {
            ignoreEncryption: true
        });
        const DOC = await file.copy();
        let pageNums = this.convertStringToNumRange(pages, DOC);
        for (let i = pageNums.length - 1; i >= 0; i--) {
            DOC.removePage(pageNums[i]);
        }
        await this.#saveFile(DOC, pathNew);
    }

    /**
     * 将字符串转换成正整数数组
     * @param str {string} 待转换的字符串，注意页码是1-based。
     *  例：
     如果要删除第2页，则填“2”；
     如果要删除第1-7页，则填“1-7”；
     如果要删除第8页及其后各页，则填“8-”；
     如果要删除第1、3、5页，则填“1,3,5"（注意是英文逗号）；
     如果要删除第1、3、5-10页，则填“1,3,5-10"（注意是英文逗号）；

     * @param doc {PDFDocument} 待处理的PDF文档
     * @returns {number[]}
     */
    static convertStringToNumRange(str, doc) {
        let subStrs = str.split(",");
        let numRange = [];
        let i = 0;
        for (const subStr of subStrs) {
            if (subStr !== "") {
                if (this.patternMiddleHiphen.test(subStr)) {
                    let ends = subStr.split('-');
                    let start = parseInt(ends[0]) - 1;
                    let end = parseInt(ends[1]) - 1;
                    for (let j = start; j <= end; j++) {
                        numRange[i] = j;
                        i++;
                    }
                } else if (this.patternInteger.test(subStr)) {
                    numRange[i] = parseInt(subStr) - 1;
                    i++;
                } else if (this.patternEndHiphen.test(subStr)) {
                    let ends = subStr.split('-');
                    let start = parseInt(ends[0]) - 1;
                    let range = doc.getPageIndices();
                    let end = range[range.length - 1];
                    for (let j = start; j <= end; j++) {
                        numRange[i] = j;
                        i++;
                    }
                }
            }
        }
        return numRange;
    }
}

export {PdfProcessor};